M2.951 - Tipologia i cicle de vida de les dades

2017-2 · Màster universitari en Ciència de dades (Data science)

Autors: Joan Bonnín i Jose L. Dolz

 

Pràctica 2: Neteja i validació de les dades

Descripció

L’objectiu d’aquesta activitat serà el tractament d’un dataset, que pot ser el creat a la pràctica 1 o bé qualsevol dataset lliure disponible a Kaggle (https://www.kaggle.com). Alguns exemples de dataset amb els que podeu treballar són:

  • Red Wine Quality (https://www.kaggle.com/uciml/red-wine-quality-cortez-et-al-2009).
  • Titanic: Machine Learning from Disaster (https://www.kaggle.com/c/titanic).
  • Predict Future Sales (https://www.kaggle.com/c/competitive-data-science-predict-future-sales/).
Els últims dos exemples corresponen a competicions actives a Kaggle de manera que, opcionalment, podríeu aprofitar el treball realitzat durant la pràctica per entrar en alguna d’aquestes competicions. Seguint les principals etapes d’un projecte analític, les diferents tasques a realitzar (i justificar) són les següents:

In [1]:
# ATENCIÓ: Es important tenir instal·lats al sistema els següents
# paquets: pandas, numpy, scipy, matplotlib, sklearn, pydotplus, graphviz
#
# Si se està utilitzan Python 3, la forma d'instal·lar un paquet és
# executant la següent ordre des de la línia de comandes:
# > pyp3 install nom_paquet

# Basic maths & data structures
import pandas as pd
import numpy as np
import scipy
import scipy.stats as stats
import itertools
import math

# Data rendering
from IPython.display import display
from IPython.display import Image  
import matplotlib.pyplot as plt

# Random forest
from sklearn.ensemble import RandomForestClassifier
from sklearn import preprocessing
from sklearn.model_selection import train_test_split
from sklearn.metrics import confusion_matrix

# Linear model
from sklearn import linear_model

# Render tree
from sklearn.externals.six import StringIO  
from sklearn.tree import export_graphviz
import pydotplus
import graphviz
In [2]:
# General constants
PVAL_THRESHOLD = 0.05
RANDOM_SEED = 2018
np.random.seed(RANDOM_SEED)

1. Descripció del dataset. Perquè és important i quina pregunta/problema pretèn respondre?

Per aquesta pràctica hem escollit el conjunt de dades de la qualitat del vi negre que, com bé s'explica en la seva pàgina de Kaggle, tracta de les variants del vi portuguès conegut com 'Vinho Verde'. Els camps que composen el dataset són tots numèrics i són els següents:

  • fixed acidity: acidesa fixa (g/l). Majoria dels àcids fixos o no volàtils fàcilment.
  • volatile acidity: acidesa volàtil (g/l). Quantitat d'àcid acètic que en gran quantitat porta al vi a tenir gust de vinagre.
  • citric acid: àcid cítric (g/l). Quantitat d'aquest àcid (normalment petita) que pot donar sabor i frescor als vins
  • residual sugar: sucre residual (g/l). Quantitat de sucre que queda després de la fermentació. És estrany trobar vins amb menys d'1 g/l i els que tenen més de 45 g/l es consideren dolços.
  • chlorides: clorurs (g/l). Representa la quantitat de sal al vi.
  • free sulfur dioxide: diòxid de sofre lliure o SO2 (mg/l). Parts per milió (ppm) del diòxid que queda lliure un cop es barreja en barrejar-se amb el vi.
  • total sulfur dioxide: Diòxid de sofre total (mg/l). La suma (en ppm) de la part lliure i la part fixada al vi de SO2.
  • density: densitat (g/l). Aquest valor dependrà de la quantitat d'alcohol i sucre en el vi.
  • pH: descriu com d'àcid o bàsic és un vi en una escala de 0 (molt àcid) a 14 (molt bàsic); La majoria dels vins són entre 3 i 4 en l'escala de pH.
  • sulphates: sulfats (g/l), que contribueixen al SO2.
  • Aquest component prevé al vi de bacteris i de l'oxidació.
  • alcohol: percentatge de contingut alcohòlic en el vi. Volum d'etanol / Volum del producte
  • quality: qualitat del vi en una puntuació entre el 0 i el 10.

Com podem veure, tenim els 12 atributs: 11 mesures físico-químiques i la qualitat, que podríem dir que és la classe.

Aquest conjunt de dades és important perquè ens pot servir perquè, mitjançant proves de correlació, podem veure quins atributs són més influents a l'hora de millorar o empitjorar la qualitat d'un vi. A més, entrenarem un model complex d'aprenentatge computacional que podrà predir la qualitat d'un vi, mitjançant aquestes dades físico-químiques obtingudes pels diferents instruments de mesurament.

2. Integració i selecció de les dades d’interès a analitzar.

In [3]:
wine_df = pd.read_csv('winequality-red.csv')
print("El conjunt de dades presenta {} camps i està compost per {} registres.".format(wine_df.shape[1], wine_df.shape[0]))
display(wine_df.head())
El conjunt de dades presenta 12 camps i està compost per 1599 registres.
fixed acidity volatile acidity citric acid residual sugar chlorides free sulfur dioxide total sulfur dioxide density pH sulphates alcohol quality
0 7.4 0.70 0.00 1.9 0.076 11.0 34.0 0.9978 3.51 0.56 9.4 5
1 7.8 0.88 0.00 2.6 0.098 25.0 67.0 0.9968 3.20 0.68 9.8 5
2 7.8 0.76 0.04 2.3 0.092 15.0 54.0 0.9970 3.26 0.65 9.8 5
3 11.2 0.28 0.56 1.9 0.075 17.0 60.0 0.9980 3.16 0.58 9.8 6
4 7.4 0.70 0.00 1.9 0.076 11.0 34.0 0.9978 3.51 0.56 9.4 5

Com podem observar en la taula, tots els atributs presenten valors numèrics continus que ens poden servir per cercar una relació lineal amb el valor discret de la qualitat. En aquest punt, no podem prescindir de cap d'aquests atributs doncs no sabem quina és la seva relació amb la nota de qualitat. Per tant, no ens desfarem de cap.


Per altra banda, crearem un atribut categòric booleà que etiqueti si un vi és bo o dolent, depenent de la seva nota. En el nostre cas, escollim que els bons vins són aquells que tenen una puntuació de qualitat de 6 o superior. Aquest atribut ens servirà com a classificador per la creació d'un model basat en *Random Forests*.

3. Neteja de les dades.

3.1. Les dades contenen zeros o elements buits? Com gestionaries aquests casos?

In [4]:
# Contem els valors nuls
n_nulls = wine_df[wine_df.isnull()].count()

# Contem els zeros
n_zeros = wine_df[wine_df == 0].count()

df_empties = pd.DataFrame()
df_empties["Nombre de Nulls"] = n_nulls
df_empties["Nombre de zeros"] = n_zeros
display(df_empties.T)
fixed acidity volatile acidity citric acid residual sugar chlorides free sulfur dioxide total sulfur dioxide density pH sulphates alcohol quality
Nombre de Nulls 0 0 0 0 0 0 0 0 0 0 0 0
Nombre de zeros 0 0 132 0 0 0 0 0 0 0 0 0

Com podem observar, no tenim cap atribut que presenti valors nuls. Per altra banda, només trobem zeros en l'atribut d'àcid cítric. Concretament, tenim 132 zeros d'un total de 1599 registres, el que representa un 8'26% del total.


En aquest cas, no cal que substituïm els zeros per cap altre valor. És totalment normal trobar vins negres sense àcid cítric, fet que els hi dóna un sabor més anyenc. De fet, com veurem més endavant, el rang de grams per litre de l'àcid cítric en vins negres és molt petit i el zero estaria dintre dels barems. Fins i tot podem trobar vins blancs amb zero grams d'àcid cítric, tot i que en aquests acostuma a haver-hi unes quantitats més altes que als vins negres.

Així doncs, podem observar que en aquest sentit, pel que fa a zeros i valors nuls, el *dataset* està completament net des de l'inici. És bastant raonable pensar que Kaggle ha realitzat una tasca prèvia de neteja per facilitar l'ús de les dades amb intencions analítiques, sense haver de dedicar gaire esforç a la neteja inicial.

</font>

3.2. Identificació i tractament de valors extrems.

In [5]:
# Dibueixem un boxplot per cada atribut per veure màxims, 
# mínims, quartils, rang interquartilic i outliers.

df_cols = list(wine_df)
n_rows = 2
n_cols = 6

fig, axes = plt.subplots(n_rows, n_cols, figsize=(30, 15))
for i, row_axes in enumerate(axes):
    for j, ax in enumerate(row_axes):
        idx = i*n_cols + j
        wine_df.boxplot(column=df_cols[idx], ax=axes[i][j], grid=False)
        
plt.show()

Als *boxplots* s'observa que la majoria de variables presenten una quantitat elevada d'*outliers*, així que val la pena aplicar-hi una anàlisi més extensa per detectar si es tracta de presència de valors erronis o la realitat de les dades és la que es mostra.

El fet que la majoria d'*outliers* s'agrupin a prop dels bigotis i la densitat baixi segons s'allunyen, ens fa pensar que els valors poden ser correctes, i que simplement la distribució presenta una desviació lateral o una variància molt elevada. Un exemple habitual per mostrar dades que presenten aquesta característica són els salaris.

4. Anàlisi de les dades.

 4.1. Selecció dels grups de dades que es volen analitzar/comparar (planificació dels anàlisis a aplicar).

No descartarem cap dels atributs que ofereix el conjunt de dades proporcionat perquè volem esbrinar quins són els que tenen més importància, quins ofereixen una distribució normal i quins no, com és la relació de variàncies, l'anàlisi de correlacions i, finalment, un model de classificació.

4.2. Comprovació de la normalitat i homogeneïtat de la variància.

El Teorema del Límit Central ens diu que qualsevol població amb un nombre d'elements prou gran tendeix a una distribució normal estàndard. En el nostre cas, considerem que aquest Teorema es pot aplicar quan N > 30. Per tant, el nostre conjunt tendeix a una distribució normal.

De totes maneres, a continuació fem servir histogrames i el test de normalitat d'Anderson-Darling per assegurar-nos:

In [6]:
####### NORMALITAT #######

# Mètodes gràfics 
def render_normality_histograms():
    n_rows = 3
    n_cols = 4
    n_bars = 30
    fig, axes = plt.subplots(n_rows, n_cols, figsize=(30, 15))
    for i, row_axes in enumerate(axes):
        for j, ax in enumerate(row_axes):
            idx = i*n_cols + j

            attr = df_cols[idx]
            ax.set_xlabel(attr)      

            data = wine_df[attr].sort_values()
            ax.hist(data, bins=n_bars, density=True)

            norm = stats.norm.pdf(data, np.mean(data), np.std(data))
            ax.plot(data, norm) 
    plt.show()
    
def render_normality_table():
    (anderson_true, anderson_false) = test_normality_anderson()
       
    anderson = merge_normality_results(anderson_true, anderson_false)
    
    normality_res = pd.DataFrame([anderson],
                                 index=["Test Anderson-Darling"])
    normality_res.columns.name = "Segueix distribució normal?"
    display(normality_res)
        
        
# Mètodes numèrics
def test_normality_anderson(p_value_threshold=PVAL_THRESHOLD, render=False):
    normal_attrs = []
    non_normal_attrs = []
    for attr_key in wine_df:
        attr = wine_df[attr_key].values
        res = stats.anderson(attr, dist='norm')
        stat = res.statistic
        threshold = res.critical_values[2] #0.05 significance == PVAL_THRESHOLD
        if stat > threshold:
            normal_attrs.append(attr_key)
        else:
            non_normal_attrs.append(attr_key)
            
    if render:
        print_attrs_distributions(normal_attrs, non_normal_attrs)
        
    return (normal_attrs, non_normal_attrs)

    
def print_attrs_distributions(normal, non_normal):
    print("Atributs amb distribució normal: {}".format(normal))
    print("Atributs amb distribució no normal: {}".format(non_normal))

    
def merge_normality_results(normal, non_normal):
    dict_normal = {k: '✓' for k in normal}
    dict_non_normal = {k: '✗' for k in non_normal}
    
    #inplace_merging
    dict_normal.update(dict_non_normal)    
    return dict_normal

render_normality_histograms()
render_normality_table()
Segueix distribució normal? alcohol chlorides citric acid density fixed acidity free sulfur dioxide pH quality residual sugar sulphates total sulfur dioxide volatile acidity
Test Anderson-Darling

El test d'Anderson-Darling ens mostra com tots els atributs segueixen una distribució normal. Això també ho corrobora els histogrames i la corba de distribució normal. Tot i això, amb grans conjunts de dades, els tests numèrics sobre normalitat no són gaire fiables. Per tant, tal com veiem a les corbes i el que hem comentat sobre el Teorema del Límit Central, considerarem que tots els atributs segueixen una distribució normal.


Per avaluar l'homogeneitat de la variància disposem de diferents tècniques i eines. Entre les més conegudes trobem *Levene* i *Fligner*, així que passem a aplicar-los mitjançant la implementació d'Scipy.

In [7]:
####### HOMOGENEITAT VARIANCIA #######

# Levene és util si la normalitat no està asegurada
def levene_test(threshold=PVAL_THRESHOLD):
    res = stats.levene(*wine_df.as_matrix(), center='median') # Expand matrix to n parameters with '*'
    return res.pvalue
#     return res
    p = res.pvalue
    pprint(("Levene", p))
    return p > threshold

def fligner_test(threshold=PVAL_THRESHOLD):
    res = stats.fligner(*wine_df.as_matrix(), center='median') # Expand matrix to n parameters with '*'
    return res.pvalue
#     return res
    p = res.pvalue
    pprint(("Flinger", p))
    return p > threshold


variance_tests = [levene_test(), fligner_test()]
are_equal = [p > PVAL_THRESHOLD for p in variance_tests]

display(pd.DataFrame(
    {
        'Populations with equal variances?': are_equal,
        'p-value': variance_tests
    },
    index=['Levene test', 'Fligner test']
).T)
Levene test Fligner test
Populations with equal variances? True True
p-value 1 1

Com podem observar, les poblacions passen el test de Levene i el test de Fligner i, per tant podem assegurar que existeix una homogeneïtat en les variàncies dels atributs.

4.3. Aplicació de proves estadístiques per comparar els grups de dades. En funció de les dades i de l’objectiu de l’estudi, aplicar proves de contrast d’hipòtesis, correlacions, regressions, etc.

Per aplicar diferents estudis sobre les dades, hem definit dues preguntes a respondre: Quines característiques determinen la qualitat del vi? i Es pot inferir, a partir de la característica més important, la qualitat del vi? </font>

Quines característiques determinen la qualitat del vi?


La primera aproximació per determinar quines variables tenen més pes sobre la qualitat final és cercar si existeix una correlació entre els atributs i la qualitat i quin és el pes de cadascun.

In [8]:
# Mirem les correlacions entre els atributs i la qualitat
data = np.ndarray(shape=(len(df_cols), 2))
for i in range(len(df_cols)):
    attr = df_cols[i]
    data[i] = stats.pearsonr(wine_df[attr], wine_df['quality'])
df_pearson = pd.DataFrame(data, index=df_cols, columns=['estimació', 'p-valor'])[:11]
df_pearson['tmp_sort'] = df_pearson['estimació'].abs()
df_pearson = df_pearson.sort_values(by='tmp_sort', ascending=False).drop('tmp_sort', axis=1)

# Format output
# pprint(df_pearson['estimació'].values)
df_pearson['estimació'] = ["{0:.4f}".format(el) for el in df_pearson['estimació'].values]
df_pearson['p-valor'] = ["{0:.2e}".format(el) for el in df_pearson['p-valor'].values]
display(df_pearson.T)
alcohol volatile acidity sulphates citric acid total sulfur dioxide density chlorides fixed acidity pH free sulfur dioxide residual sugar
estimació 0.4762 -0.3906 0.2514 0.2264 -0.1851 -0.1749 -0.1289 0.1241 -0.0577 -0.0507 0.0137
p-valor 2.83e-91 2.05e-59 1.80e-24 4.99e-20 8.62e-14 1.87e-12 2.31e-07 6.50e-07 2.10e-02 4.28e-02 5.83e-01

Hem ordenat els atributs dels que més impacten a l'hora de determinar la qualitat als que menys. Tal i com podem observar, la qualitat augmenta gairebé un 50% segons el volum d'**alcohol** que tenim en el vi. Per altra banda, el 40% de quantitat d'**acidesa volàtil** influeix negativament en la qualitat. És a dir, com més sàpiga a vinagre, pitjor.

Seguidament tenim que el 25% de la quantitat de **sulfats** augmenten la qualitat. Sembla obvi, doncs aquest component ajuda que el vi no s'oxidi i impedeix l'aparició de bacteris. Però per altra banda, paradoxalment, el **SO2 total i el lliure** resten qualitat al vi (un 18'5% i un 5% de la seva quantitat, respectivament).

L'**àcid cítric** suma el 22'6% del seu valor i podem deduir que un vi que sigui més fresc afavoreix a obtenir una millor nota. Per altra banda, com menys dens és el vi -menys líquid- pitjor la seva qualitat, restant un 17'5% del valor de la **densitat**. La quantitat de **clorurs** també afecta negativament, restant quasi un 13% del seu valor: com més salat estigui el vi, pitjor qualitat tindrà. Per altra banda, el valor del **sucre** només suma un 1'4% del seu valor. No ens ha de resultar xocant doncs estem parlant de vins negres i aquests no acostumen a ser dolços (tret que siguin escumosos, però no és el cas d'aquests).

Darrerament, tenim que el **pH**, que resta un 5% del seu valor a la qualitat final. És a dir, com més àcid sigui un vi, pitjor encara que no compti molt. Però, per altra banda, l'**acidesa fixa** suma a la qualitat del vi un 12'4% del seu valor.

Pel que fa als p-valors, podem descartar tots aquells atributs que superin el llindar de 0.05. Només el sucre supera aquest llindar i, per tant, no el faríem servir en un model de regressió lineal.

In [9]:
# Correlació dels atributs
pd.plotting.scatter_matrix(wine_df.iloc[:, :11], figsize=(30,15))
plt.show()

En el gràfic de correlacions entre parells d'atributs -considerant que són tots normals- podem veure clarament la relació directa que havíem explicat al principi entre el SO2 lliure i total: com més tenim d'un, més tenim de l'altre.


Per altra banda, tenim atributs que es relacionen fortament entre si. Per exemple, com més àcid cítric, més acidesa fixa. Això ens fa pensar que pujant aquest dos paràmetres obtindríem millor nota perquè, a més, com més acidesa fixa, menys pH, que li resta qualitat. A més a més, com més àcid cítric, menys acidesa volàtil i, per tant, menys sabor a vinagre. Però tenim el problema que com més quantitat d'acidesa fixa, derivada de l'àcid cítric, augmentem també la seva densitat, que va en detriment de la qualitat. Com veiem, trobar els paràmetres físico-químics equilibrats per treure un vi de molt bona qualitat és una tasca força feixuga.

A continuació, construirem un model de predicció. Serà una combinació d'arbres de decisió entrenats amb bagging (usant mostreig amb reemplaçament), també conegut com Random Forests. Hem escollit aquest classificador per diferents motius: curiositat acadèmica, aproximar-nos al treball amb eines d'entorns reals i, principalment, que és un model amb el coneixement accessible.

Això vol dir que, a diferència d'altres models molt coneguts com les xarxes neuronals, el model descriu perfectament perquè es comporta com ho fa. Donat que la pregunta que volem resoldre és conèixer els factors amb més pes per definir la qualitat del vi, el model *Random Forest* sembla el candidat idoni per aquesta tasca.

In [10]:
#Inserció d'atribut qualitatiu per la qualitat del vi
CLASSES_NAMES = ['Bad wine', 'Good wine']
GOOD_THRESHOLD = 6
raw_y = wine_df['quality'] >= GOOD_THRESHOLD
In [11]:
def encode_raw_y(raw_y):    
    le = preprocessing.LabelEncoder()
    y = le.fit_transform(raw_y)
    return y

def random_forest_preprocess_data(df, raw_y):
    #Input data
    X = df.drop(['quality'], axis=1)
    #Labels
    y = encode_raw_y(raw_y)
    return (X, y)

def split_train_test(X, y):
    X_train, X_test, y_train, y_test = train_test_split(
        X, y, test_size=0.15, random_state=RANDOM_SEED
    )
    return (X_train, X_test, y_train, y_test)

def display_precission(score):
    print("Precisió del model: {} %".format(round(score*100, 2)))

(X, y) = random_forest_preprocess_data(wine_df, raw_y)
(X_train, X_test, y_train, y_test) = split_train_test(X, y)

rfc_model = RandomForestClassifier(bootstrap=True, n_estimators = 150, n_jobs=8, random_state=RANDOM_SEED)
rfc_model.fit(X_train, y_train)
Out[11]:
RandomForestClassifier(bootstrap=True, class_weight=None, criterion='gini',
            max_depth=None, max_features='auto', max_leaf_nodes=None,
            min_impurity_decrease=0.0, min_impurity_split=None,
            min_samples_leaf=1, min_samples_split=2,
            min_weight_fraction_leaf=0.0, n_estimators=150, n_jobs=8,
            oob_score=False, random_state=2018, verbose=0,
            warm_start=False)
In [12]:
rfc_score = rfc_model.score(X_test, y_test)
display_precission(rfc_score)
# pprint(rfc_model.feature_importances_)
importances_df = pd.DataFrame(
    rfc_model.feature_importances_,
    index=list(X_test),
    columns=['Importància']
)
importances_df = importances_df.sort_values(by="Importància", ascending=False)
display(importances_df.T)
Precisió del model: 78.75 %
alcohol sulphates volatile acidity total sulfur dioxide density chlorides pH fixed acidity citric acid free sulfur dioxide residual sugar
Importància 0.189099 0.126577 0.120986 0.099195 0.082641 0.071539 0.069156 0.066683 0.062752 0.057955 0.053415

Com podem veure, la importància que atorga als atributs el nostre model basat en boscos aleatoris (*Random Forests*) són bastant semblants als obtinguts en la correlació lineal, sent l'alcohol el de major pes i el sucre residual el que menys. No hem de confondre els pesos que dóna el *Random Forest* als atributs (on la seva suma és 1) amb els factors donats en la correlació lineal amb la qualitat.


Per altra banda, obtenim una precisió prou alta encara que una mica per sota del llindar del 80% al qual estem acostumats a donar per bo el model. Hem de tenir en compte que un model complex com són els *Random Forests* millora la seva precisió com més gran és el seu conjunt d'entrenament. Per tant, podem assegurar que utilitzant les dades dels vins de collites d'altres anys -recordem que estem fent servir només l'any 2009- millorarem la precisió de manera que aquest model podrà determinar de forma més exacta la qualitat de vins futurs.

Una vegada hem realitzat aquestes diferents proves i anàlisis, podem dir clarament que la característica amb més pes per determinar la qualitat del vi, sorprenentment, és el nivell d'alcohol.

Es pot inferir, a partir del volum d'alcohol, la qualitat del vi?


Per tractar de respondre a aquesta pregunta, analitzarem la relació entre les dimensions `alcohol ~ qualitat` i, si escau, crearem un model de regressió lineal per tal de veure com es relacionen.

In [13]:
alcohol_quality_df = wine_df[['alcohol', 'quality']]

fig, ax = plt.subplots(1, 1, figsize=(25, 7))
ax.scatter(alcohol_quality_df['alcohol'], alcohol_quality_df['quality'])
ax.set_xlabel('Graduació alcoholica')
ax.set_ylabel('Qualitat')
plt.show()

Com podem observar, en aquest cas és obvi que sobre aquestes dades no es pot aplicar un model de regressió lineal, ja que el resultat serà de molt baixa qualitat. Això és degut al fet que la variable qualitat és discreta, i no continua. Tanmateix, s'observa una mena de diagonal que podria fer-nos intuir que la qualitat augmenta així com ho fa la graduació alcohòlica. En cas de voler realitzar una regressió lineal amb les eines usades en aquest exercici, s'hauria d'implementar un codi molt semblant al descrit a la documentació de sklearn.


Creiem que, com que ja disposem d'un model per inferir la qualitat del vi, el que podríem fer és tractar d'entrenar el model només amb la graduació alcohòlica i analitzar-ne el comportament.</font>

In [14]:
(X_alcohol_train, X_alcohol_test, y_alcohol_train, y_alcohol_test) = split_train_test(alcohol_quality_df['alcohol'], encode_raw_y(raw_y))
X_alcohol_train = X_alcohol_train.values.reshape(-1, 1)
X_alcohol_test = X_alcohol_test.values.reshape(-1, 1)

rfc_alcohol_model = RandomForestClassifier(bootstrap=True, n_estimators = 150, n_jobs=8, random_state=RANDOM_SEED)
rfc_alcohol_model.fit(X_alcohol_train, y_alcohol_train)
rfc_alcohol_score = rfc_alcohol_model.score(X_alcohol_test, y_alcohol_test)

display_precission(rfc_alcohol_score)
Precisió del model: 67.08 %

Notem com la precisió ha caigut considerablement, com era d'esperar. A més, també hem de tenir present que encara que 67% no sembli un valor especialment baix, no és gaire vàlid. Donat que estam intentant modelar sobre dues classes, un classificador aleatori ens haurà de donar un 50% de precisió, així que aquest és el nostre punt de partida.


Com a resposta a la pregunta original, que es demana "Es pot inferir, a partir del volum d'alcohol, la qualitat del vi?", amb els resultats obtinguts, tot i no ser una precisió menyspreable, consideram que el model no és suficientment vàlid com per determinar la qualitat del vi només coneixent-ne el volum d'alcohol.

5. Representació dels resultats a partir de taules i gràfiques.


Durant les diferents etapes de neteja i anàlisi hem mostrat les taules i gràfiques adients per entendre el comportament de les accions. Tot i això, a continuació inserim una sèrie de gràfiques addicionals que permeten obtenir encara més coneixement del conjunt de dades.

Anàlisi de normalitat

In [15]:
def plot_qq(keys):
    if keys is None:
        keys = list(wine_df)
        
    cols = 4
    rows = math.ceil(len(keys)/cols)
    plt.figure(figsize=(25, 7))
    for i, attr_key in enumerate(keys):
        plt.subplot(rows, cols, i+1)
        attr = wine_df[attr_key]
        stats.probplot(attr, dist="norm", plot=plt)
    plt.show()

plot_qq(['alcohol', 'density', 'quality'])

La gràfica Q-Q és molt usada per comprovar la normalitat d'unes sèries de dades. Tanmateix, amb la informació trobada a l'apartat 4.2 ha sigut suficient per veure la normalitat de les dades, així que aquesta implementació ha quedat relegada a una mena d'apèndix. Com més s'aproximen els quantils teòrics a la bisectriu, més normal és la distribució. Fins i tot, podem veure que la distribució de l'atribut de la qualitat, tot i ser discret, és distribueix de forma equitativa al llarg de la bisectriu.

Arbre de decisió

Seleccionat una mostra aleatòria dels 250 arbres del *RandomForest*, observam com l'arbre de decisió és relativament complex. Tot i això, renderitzar-lo és una bona manera de seguir les decisions que aquests pren en funció de les dades d'entrada. Per simplificar el model visualment, podem generar un nou *RandomForest* definit una profunditat màxima dels arbres. Així, òbviament perdrem qualitat de predicció, però és un bon exercici acadèmic.

In [16]:
def render_tree(tree):
    dot_data = StringIO()
    export_graphviz(tree, out_file=dot_data,  
                    filled=True, rounded=True,
                    class_names=CLASSES_NAMES,
                    feature_names=list(wine_df.drop(columns='quality')),
                    special_characters=True)

    graph = pydotplus.graph_from_dot_data(dot_data.getvalue())  
    display(Image(graph.create_png()))
In [17]:
# Render random forest sample tree
sample_tree = np.random.choice(rfc_model.estimators_)
render_tree(sample_tree)
In [18]:
rfc_model_basic_trees = RandomForestClassifier(max_depth=3, bootstrap=True, n_estimators = 150, n_jobs=8, random_state=RANDOM_SEED)
rfc_model_basic_trees.fit(X_train, y_train)

sample_tree = np.random.choice(rfc_model_basic_trees.estimators_)
render_tree(sample_tree)

print("Precisió del model: {} %".format(round(rfc_model_basic_trees.score(X_test, y_test)*100, 2)))
Precisió del model: 71.25 %

Ara sí, podem avaluar i seguir fàcilment les decisions que pren aquest nou model. Sorprenentment observem com la precisió del conjunt del *Random Forest* no ha baixat gaire, i es situa en un 71.25%, només 7.5 punts per sota del model original.

Matriu de confusió


Una altra eina molt útil a l'hora d'avaluar models de regressió és la matriu de confusió. Aquesta ens mostra, per cada classe, el nombre de Vertaders positius (*TP*), Falsos postius (*FP*), Vertaders Negatius (*TN*) i Falsos negatius (*FN*).

In [19]:
# http://scikit-learn.org/stable/auto_examples/model_selection/plot_confusion_matrix.html
def plot_confusion_matrix(cm, classes,
                          normalize=False,
                          title='Matriu de confusió',
                          cmap=plt.cm.Blues):
    """
    This function prints and plots the confusion matrix.
    Normalization can be applied by setting `normalize=True`.
    """
    if normalize:
        cm = cm.astype('float') / cm.sum(axis=1)[:, np.newaxis]

    plt.imshow(cm, interpolation='nearest', cmap=cmap)
    plt.title(title)
    plt.colorbar()
    tick_marks = np.arange(len(classes))
    plt.xticks(tick_marks, classes, rotation=45)
    plt.yticks(tick_marks, classes)

    fmt = '.2f' if normalize else 'd'
    thresh = cm.max() / 2.
    for i, j in itertools.product(range(cm.shape[0]), range(cm.shape[1])):
        plt.text(j, i, format(cm[i, j], fmt),
                 horizontalalignment="center",
                 color="white" if cm[i, j] > thresh else "black")

    plt.tight_layout()
    plt.ylabel('True label')
    plt.xlabel('Predicted label')
In [20]:
y_pred = rfc_model.predict(X_test)
cnf_matrix = confusion_matrix(y_test, y_pred)

plot_confusion_matrix(cnf_matrix, CLASSES_NAMES, normalize=True)

Podem observar com les prediccions, tant encertades com errònies estan molt equilibrades, així que no sembla que cap de les dues classes sigui un factor determinant a l'hora d'usar el model generat. Això és degut al fet que el *threshold* definit per dir si un vi és bo o dolent és molt proper a la mitjana i coincideix amb la mediana.

6. Resolució del problema. A partir dels resultats obtinguts, quines són les conclusions? Els resultats permeten respondre al problema?

Com hem pogut veure al llarg de tot el document, tots els atributs presents ajuden a determinar la qualitat del vi. Tot i això, hem observat com atributs com els atributs d'alcohol, sulfats o l'acidesa volàtil tenen més pes a la qualitat del vi. A més, aquesta idea ha estat doblement validada, ja que hem descobert aquesta informació mitjançant l'anàlisi de correlacions i mitjançant el *Random Forest*.


Per altre costat, hem pogut veure que podem predir la qualitat del vi a partir de l'alcohol amb una precisió del 67%. Tot i que hi ha estudis que donen per bones precisions per sobre del 60%, el nostre llindar és a partir del 80% de precisió per considerar un model com fiable. Per tant, s'hauran de recórrer a més atributs per poder predir la qualitat de forma lineal.

Per altra banda, gràcies a respondre les preguntes originals, també hem pogut construir un model de predicció bastant robust que pot classificar els vins entre bons i, diguem-ne, no tan bons, amb una fiabilitat de gairebé el 79%. Com ja s'ha comentat, si a aquest model li donem dades d'anys posteriors a l'any 2009 per entrenar-se, obtindrem un classificador molt fiable per determinar la qualitat dels vins d'enguany.

7. Codi: Cal adjuntar el codi, preferiblement en R, amb el que s’ha realitzat la neteja, anàlisi i representació de les dades. Si ho preferiu, també podeu treballar en Python

Tot el codi emprat en aquesta pràctica es pot trobar en les diferents preguntes en les cel·les precedides amb `In [ ]` i es poden executar des de jupyter o qualsevol terminal interactiva de Python.

8. Expotació del fitxer amb les dades tractades


Donat que no s'ha hagut d'aplicar cap modificació a les dades originals i s'han fet servir totes les variables, podem dir que el *dataset* amb les dades tractades és el mateix que l'original. Tot i això, per demostrar l'ús de l'eina d'exportaicó, adjuntarem la classe que defineix si el vi és bo o dolent com una columna més, i procedirem a exportar el *dataset*.

In [21]:
TREATED_CSV_PATH = 'dataset-tractat.csv'

exportable_df = wine_df.copy()
exportable_df['label'] = raw_y
exportable_df['label'] = exportable_df['label'].map({True: 'GoodWine', False: 'BadWine'})

exportable_df.to_csv(TREATED_CSV_PATH, sep=';', index=False)